Skip to content

Commit 0e8c7de

Browse files
authored
[Blazor] Fix PropertyGetter to handle value types correctly in SupplyParameterFromPersistentComponentStateValueProvider (#62369)
* Adds support for serializing fields on Persistent Component State to support ValueTuple in PCS. * Fixes an issue where `PropertyGetter` incorrectly generated a delegate for value types. * Adds unit tests to cover the scenarios. Fixes #62368.
1 parent 7641b9f commit 0e8c7de

File tree

4 files changed

+180
-5
lines changed

4 files changed

+180
-5
lines changed

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/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;

src/Components/Shared/src/JsonSerializerOptionsProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ internal static class JsonSerializerOptionsProvider
1111
{
1212
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
1313
PropertyNameCaseInsensitive = true,
14+
IncludeFields = true,
1415
};
1516
}

src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,6 @@
107107
public class TestServiceProvider : IServiceProvider
108108
{
109109
public object GetService(Type serviceType)
110-
=> throw new NotImplementedException();
110+
=> null;
111111
}
112112
}

0 commit comments

Comments
 (0)