Skip to content

Commit 342f130

Browse files
Merging main into darc-main-6a500fc6-cc1d-4c69-9b8a-a7d84549614e
2 parents af02f41 + ef0da55 commit 342f130

File tree

21 files changed

+590
-30
lines changed

21 files changed

+590
-30
lines changed

src/Components/Components/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ static Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponen
1818
static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsMetrics(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
1919
static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
2020
static Microsoft.Extensions.DependencyInjection.SupplyParameterFromPersistentComponentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
21+
virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.GetComponentKey() -> object?

src/Components/Components/src/Reflection/PropertyGetter.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ public PropertyGetter(Type targetType, PropertyInfo property)
3333

3434
var propertyGetterAsFunc =
3535
getMethod.CreateDelegate(typeof(Func<,>).MakeGenericType(targetType, property.PropertyType));
36+
3637
var callPropertyGetterClosedGenericMethod =
3738
CallPropertyGetterOpenGenericMethod.MakeGenericMethod(targetType, property.PropertyType);
39+
3840
_GetterDelegate = (Func<object, object>)
3941
callPropertyGetterClosedGenericMethod.CreateDelegate(typeof(Func<object, object>), propertyGetterAsFunc);
4042
}
@@ -46,11 +48,11 @@ public PropertyGetter(Type targetType, PropertyInfo property)
4648

4749
public object? GetValue(object target) => _GetterDelegate(target);
4850

49-
private static TValue CallPropertyGetter<TTarget, TValue>(
51+
private static object? CallPropertyGetter<TTarget, TValue>(
5052
Func<TTarget, TValue> Getter,
5153
object target)
5254
where TTarget : notnull
5355
{
54-
return Getter((TTarget)target);
56+
return (object?)Getter((TTarget)target);
5557
}
5658
}

src/Components/Components/src/Rendering/ComponentState.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,36 @@ internal ValueTask DisposeInBatchAsync(RenderBatchBuilder batchBuilder)
339339
return DisposeAsync();
340340
}
341341

342+
/// <summary>
343+
/// Gets the component key for this component instance.
344+
/// This is used for state persistence and component identification across render modes.
345+
/// </summary>
346+
/// <returns>The component key, or null if no key is available.</returns>
347+
protected internal virtual object? GetComponentKey()
348+
{
349+
if (ParentComponentState is not { } parentComponentState)
350+
{
351+
return null;
352+
}
353+
354+
// Check if the parentComponentState has a `@key` directive applied to the current component.
355+
var frames = parentComponentState.CurrentRenderTree.GetFrames();
356+
for (var i = 0; i < frames.Count; i++)
357+
{
358+
ref var currentFrame = ref frames.Array[i];
359+
if (currentFrame.FrameType != RenderTreeFrameType.Component ||
360+
!ReferenceEquals(Component, currentFrame.Component))
361+
{
362+
// Skip any frame that is not the current component.
363+
continue;
364+
}
365+
366+
return currentFrame.ComponentKey;
367+
}
368+
369+
return null;
370+
}
371+
342372
private string GetDebuggerDisplay()
343373
{
344374
return $"ComponentId = {ComponentId}, Type = {Component.GetType().Name}, Disposed = {_componentWasDisposed}";

src/Components/Components/src/SupplyParameterFromPersistentComponentStateValueProvider.cs

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ public bool CanSupplyValue(in CascadingParameterInfo parameterInfo)
4545
return state.TryTakeFromJson(storageKey, parameterInfo.PropertyType, out var value) ? value : null;
4646
}
4747

48-
[UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")] [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "OpenComponent already has the right set of attributes")] [UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")]
48+
[UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")]
49+
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "OpenComponent already has the right set of attributes")]
50+
[UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")]
4951
public void Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
5052
{
5153
var propertyName = parameterInfo.PropertyName;
@@ -221,35 +223,46 @@ private static void GrowBuffer(ref byte[]? pool, ref Span<byte> keyBuffer, int?
221223

222224
private static object? GetSerializableKey(ComponentState componentState)
223225
{
224-
if (componentState.ParentComponentState is not { } parentComponentState)
226+
var componentKey = componentState.GetComponentKey();
227+
if (componentKey != null && IsSerializableKey(componentKey))
225228
{
226-
return null;
229+
return componentKey;
227230
}
228231

229-
// Check if the parentComponentState has a `@key` directive applied to the current component.
230-
var frames = parentComponentState.CurrentRenderTree.GetFrames();
231-
for (var i = 0; i < frames.Count; i++)
232+
return null;
233+
}
234+
235+
private static string GetComponentType(ComponentState componentState) => componentState.Component.GetType().FullName!;
236+
237+
private static string GetParentComponentType(ComponentState componentState)
238+
{
239+
if (componentState.ParentComponentState == null)
240+
{
241+
return "";
242+
}
243+
if (componentState.ParentComponentState.Component == null)
232244
{
233-
ref var currentFrame = ref frames.Array[i];
234-
if (currentFrame.FrameType != RenderTree.RenderTreeFrameType.Component ||
235-
!ReferenceEquals(componentState.Component, currentFrame.Component))
245+
return "";
246+
}
247+
248+
if (componentState.ParentComponentState.ParentComponentState != null)
249+
{
250+
var renderer = componentState.Renderer;
251+
var parentRenderMode = renderer.GetComponentRenderMode(componentState.ParentComponentState.Component);
252+
var grandParentRenderMode = renderer.GetComponentRenderMode(componentState.ParentComponentState.ParentComponentState.Component);
253+
if (parentRenderMode != grandParentRenderMode)
236254
{
237-
// Skip any frame that is not the current component.
238-
continue;
255+
// This is the case when EndpointHtmlRenderer introduces an SSRRenderBoundary component.
256+
// We want to return "" because the SSRRenderBoundary component is not a real component
257+
// and won't appear on the component tree in the WebAssemblyRenderer and RemoteRenderer
258+
// interactive scenarios.
259+
return "";
239260
}
240-
241-
var componentKey = currentFrame.ComponentKey;
242-
return !IsSerializableKey(componentKey) ? null : componentKey;
243261
}
244262

245-
return null;
263+
return GetComponentType(componentState.ParentComponentState);
246264
}
247265

248-
private static string GetComponentType(ComponentState componentState) => componentState.Component.GetType().FullName!;
249-
250-
private static string GetParentComponentType(ComponentState componentState) =>
251-
componentState.ParentComponentState == null ? "" : GetComponentType(componentState.ParentComponentState);
252-
253266
private static byte[] KeyFactory((string parentComponentType, string componentType, string propertyName) parts) =>
254267
SHA256.HashData(Encoding.UTF8.GetBytes(string.Join(".", parts.parentComponentType, parts.componentType, parts.propertyName)));
255268

src/Components/Components/test/SupplyParameterFromPersistentComponentStateValueProviderTests.cs

Lines changed: 174 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Linq;
67
using System.Text;
78
using System.Text.Json;
89
using Microsoft.AspNetCore.Components.Infrastructure;
@@ -431,6 +432,146 @@ public async Task PersistenceFails_MultipleComponentsUseInvalidKeyTypes(object c
431432
Assert.Contains(sink.Writes, w => w is { LogLevel: LogLevel.Error } && w.EventId == new EventId(1000, "PersistenceCallbackError"));
432433
}
433434

435+
[Fact]
436+
public async Task PersistAsync_CanPersistValueTypes_IntProperty()
437+
{
438+
// Arrange
439+
var state = new Dictionary<string, byte[]>();
440+
var store = new TestStore(state);
441+
var persistenceManager = new ComponentStatePersistenceManager(
442+
NullLogger<ComponentStatePersistenceManager>.Instance,
443+
new ServiceCollection().BuildServiceProvider());
444+
445+
var renderer = new TestRenderer();
446+
var component = new ValueTypeTestComponent { IntValue = 42 };
447+
var componentStates = CreateComponentState(renderer, [(component, null)], null);
448+
var componentState = componentStates.First();
449+
450+
// Create the provider and subscribe the component
451+
var provider = new SupplyParameterFromPersistentComponentStateValueProvider(persistenceManager.State);
452+
var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(ValueTypeTestComponent.IntValue), typeof(int));
453+
provider.Subscribe(componentState, cascadingParameterInfo);
454+
455+
// Act
456+
await persistenceManager.PersistStateAsync(store, renderer);
457+
458+
// Assert
459+
Assert.NotEmpty(store.State);
460+
461+
// Verify the value was persisted correctly
462+
var newState = new PersistentComponentState(new Dictionary<string, byte[]>(), []);
463+
newState.InitializeExistingState(store.State);
464+
465+
var key = SupplyParameterFromPersistentComponentStateValueProvider.ComputeKey(componentState, cascadingParameterInfo.PropertyName);
466+
Assert.True(newState.TryTakeFromJson<int>(key, out var retrievedValue));
467+
Assert.Equal(42, retrievedValue);
468+
}
469+
470+
[Fact]
471+
public async Task PersistAsync_CanPersistValueTypes_NullableIntProperty()
472+
{
473+
// Arrange
474+
var state = new Dictionary<string, byte[]>();
475+
var store = new TestStore(state);
476+
var persistenceManager = new ComponentStatePersistenceManager(
477+
NullLogger<ComponentStatePersistenceManager>.Instance,
478+
new ServiceCollection().BuildServiceProvider());
479+
480+
var renderer = new TestRenderer();
481+
var component = new ValueTypeTestComponent { NullableIntValue = 123 };
482+
var componentStates = CreateComponentState(renderer, [(component, null)], null);
483+
var componentState = componentStates.First();
484+
485+
// Create the provider and subscribe the component
486+
var provider = new SupplyParameterFromPersistentComponentStateValueProvider(persistenceManager.State);
487+
var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(ValueTypeTestComponent.NullableIntValue), typeof(int?));
488+
provider.Subscribe(componentState, cascadingParameterInfo);
489+
490+
// Act
491+
await persistenceManager.PersistStateAsync(store, renderer);
492+
493+
// Assert
494+
Assert.NotEmpty(store.State);
495+
496+
// Verify the value was persisted correctly
497+
var newState = new PersistentComponentState(new Dictionary<string, byte[]>(), []);
498+
newState.InitializeExistingState(store.State);
499+
500+
var key = SupplyParameterFromPersistentComponentStateValueProvider.ComputeKey(componentState, cascadingParameterInfo.PropertyName);
501+
Assert.True(newState.TryTakeFromJson<int?>(key, out var retrievedValue));
502+
Assert.Equal(123, retrievedValue);
503+
}
504+
505+
[Fact]
506+
public async Task PersistAsync_CanPersistValueTypes_TupleProperty()
507+
{
508+
// Arrange
509+
var state = new Dictionary<string, byte[]>();
510+
var store = new TestStore(state);
511+
var persistenceManager = new ComponentStatePersistenceManager(
512+
NullLogger<ComponentStatePersistenceManager>.Instance,
513+
new ServiceCollection().BuildServiceProvider());
514+
515+
var renderer = new TestRenderer();
516+
var component = new ValueTypeTestComponent { TupleValue = ("test", 456) };
517+
var componentStates = CreateComponentState(renderer, [(component, null)], null);
518+
var componentState = componentStates.First();
519+
520+
// Create the provider and subscribe the component
521+
var provider = new SupplyParameterFromPersistentComponentStateValueProvider(persistenceManager.State);
522+
var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(ValueTypeTestComponent.TupleValue), typeof((string, int)));
523+
provider.Subscribe(componentState, cascadingParameterInfo);
524+
525+
// Act
526+
await persistenceManager.PersistStateAsync(store, renderer);
527+
528+
// Assert
529+
Assert.NotEmpty(store.State);
530+
531+
// Verify the value was persisted correctly
532+
var newState = new PersistentComponentState(new Dictionary<string, byte[]>(), []);
533+
newState.InitializeExistingState(store.State);
534+
535+
var key = SupplyParameterFromPersistentComponentStateValueProvider.ComputeKey(componentState, cascadingParameterInfo.PropertyName);
536+
Assert.True(newState.TryTakeFromJson<(string, int)>(key, out var retrievedValue));
537+
Assert.Equal(("test", 456), retrievedValue);
538+
}
539+
540+
[Fact]
541+
public async Task PersistAsync_CanPersistValueTypes_NullableTupleProperty()
542+
{
543+
// Arrange
544+
var state = new Dictionary<string, byte[]>();
545+
var store = new TestStore(state);
546+
var persistenceManager = new ComponentStatePersistenceManager(
547+
NullLogger<ComponentStatePersistenceManager>.Instance,
548+
new ServiceCollection().BuildServiceProvider());
549+
550+
var renderer = new TestRenderer();
551+
var component = new ValueTypeTestComponent { NullableTupleValue = ("test2", 789) };
552+
var componentStates = CreateComponentState(renderer, [(component, null)], null);
553+
var componentState = componentStates.First();
554+
555+
// Create the provider and subscribe the component
556+
var provider = new SupplyParameterFromPersistentComponentStateValueProvider(persistenceManager.State);
557+
var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(ValueTypeTestComponent.NullableTupleValue), typeof((string, int)?));
558+
provider.Subscribe(componentState, cascadingParameterInfo);
559+
560+
// Act
561+
await persistenceManager.PersistStateAsync(store, renderer);
562+
563+
// Assert
564+
Assert.NotEmpty(store.State);
565+
566+
// Verify the value was persisted correctly
567+
var newState = new PersistentComponentState(new Dictionary<string, byte[]>(), []);
568+
newState.InitializeExistingState(store.State);
569+
570+
var key = SupplyParameterFromPersistentComponentStateValueProvider.ComputeKey(componentState, cascadingParameterInfo.PropertyName);
571+
Assert.True(newState.TryTakeFromJson<(string, int)?>(key, out var retrievedValue));
572+
Assert.Equal(("test2", 789), retrievedValue);
573+
}
574+
434575
private static void InitializeState(PersistentComponentState state, List<(ComponentState componentState, string propertyName, string value)> items)
435576
{
436577
var dictionary = new Dictionary<string, byte[]>();
@@ -452,7 +593,7 @@ private static CascadingParameterInfo CreateCascadingParameterInfo(string proper
452593

453594
private static List<ComponentState> CreateComponentState(
454595
TestRenderer renderer,
455-
List<(TestComponent, object)> components,
596+
List<(IComponent, object)> components,
456597
ParentComponent parentComponent = null)
457598
{
458599
var i = 1;
@@ -464,7 +605,20 @@ private static List<ComponentState> CreateComponentState(
464605
var componentState = new ComponentState(renderer, i++, component, parentComponentState);
465606
if (currentRenderTree != null && key != null)
466607
{
467-
currentRenderTree.OpenComponent<TestComponent>(0);
608+
// Open component based on the actual component type
609+
if (component is TestComponent)
610+
{
611+
currentRenderTree.OpenComponent<TestComponent>(0);
612+
}
613+
else if (component is ValueTypeTestComponent)
614+
{
615+
currentRenderTree.OpenComponent<ValueTypeTestComponent>(0);
616+
}
617+
else
618+
{
619+
currentRenderTree.OpenComponent<IComponent>(0);
620+
}
621+
468622
var frames = currentRenderTree.GetFrames();
469623
frames.Array[frames.Count - 1].ComponentStateField = componentState;
470624
if (key != null)
@@ -497,6 +651,24 @@ private class TestComponent : IComponent
497651
public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException();
498652
}
499653

654+
private class ValueTypeTestComponent : IComponent
655+
{
656+
[SupplyParameterFromPersistentComponentState]
657+
public int IntValue { get; set; }
658+
659+
[SupplyParameterFromPersistentComponentState]
660+
public int? NullableIntValue { get; set; }
661+
662+
[SupplyParameterFromPersistentComponentState]
663+
public (string, int) TupleValue { get; set; }
664+
665+
[SupplyParameterFromPersistentComponentState]
666+
public (string, int)? NullableTupleValue { get; set; }
667+
668+
public void Attach(RenderHandle renderHandle) => throw new NotImplementedException();
669+
public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException();
670+
}
671+
500672
private class TestStore(Dictionary<string, byte[]> initialState) : IPersistentComponentStateStore
501673
{
502674
public IDictionary<string, byte[]> State { get; set; } = initialState;

0 commit comments

Comments
 (0)