Skip to content

Commit 01ee026

Browse files
Copilotjaviercn
andcommitted
Fix PropertyGetter to handle value types correctly and add value type tests
Co-authored-by: javiercn <[email protected]>
1 parent 02b4e59 commit 01ee026

File tree

2 files changed

+219
-8
lines changed

2 files changed

+219
-8
lines changed

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

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ internal sealed class PropertyGetter
1111
{
1212
private static readonly MethodInfo CallPropertyGetterOpenGenericMethod =
1313
typeof(PropertyGetter).GetMethod(nameof(CallPropertyGetter), BindingFlags.NonPublic | BindingFlags.Static)!;
14+
15+
private static readonly MethodInfo CallPropertyGetterByReferenceOpenGenericMethod =
16+
typeof(PropertyGetter).GetMethod(nameof(CallPropertyGetterByReference), BindingFlags.NonPublic | BindingFlags.Static)!;
17+
18+
// Delegate type for a by-ref property getter
19+
private delegate TValue ByRefFunc<TDeclaringType, TValue>(ref TDeclaringType arg);
1420

1521
private readonly Func<object, object?> _GetterDelegate;
1622

@@ -31,12 +37,29 @@ public PropertyGetter(Type targetType, PropertyInfo property)
3137
{
3238
var getMethod = property.GetMethod;
3339

34-
var propertyGetterAsFunc =
35-
getMethod.CreateDelegate(typeof(Func<,>).MakeGenericType(targetType, property.PropertyType));
36-
var callPropertyGetterClosedGenericMethod =
37-
CallPropertyGetterOpenGenericMethod.MakeGenericMethod(targetType, property.PropertyType);
38-
_GetterDelegate = (Func<object, object>)
39-
callPropertyGetterClosedGenericMethod.CreateDelegate(typeof(Func<object, object>), propertyGetterAsFunc);
40+
// Instance methods in the CLR can be turned into static methods where the first parameter
41+
// is open over "target". This parameter is always passed by reference, so we have a code
42+
// path for value types and a code path for reference types.
43+
if (getMethod.DeclaringType!.IsValueType)
44+
{
45+
// Create a delegate (ref TDeclaringType) -> TValue
46+
var propertyGetterAsFunc =
47+
getMethod.CreateDelegate(typeof(ByRefFunc<,>).MakeGenericType(targetType, property.PropertyType));
48+
var callPropertyGetterClosedGenericMethod =
49+
CallPropertyGetterByReferenceOpenGenericMethod.MakeGenericMethod(targetType, property.PropertyType);
50+
_GetterDelegate = (Func<object, object?>)
51+
callPropertyGetterClosedGenericMethod.CreateDelegate(typeof(Func<object, object?>), propertyGetterAsFunc);
52+
}
53+
else
54+
{
55+
// Create a delegate TDeclaringType -> TValue
56+
var propertyGetterAsFunc =
57+
getMethod.CreateDelegate(typeof(Func<,>).MakeGenericType(targetType, property.PropertyType));
58+
var callPropertyGetterClosedGenericMethod =
59+
CallPropertyGetterOpenGenericMethod.MakeGenericMethod(targetType, property.PropertyType);
60+
_GetterDelegate = (Func<object, object?>)
61+
callPropertyGetterClosedGenericMethod.CreateDelegate(typeof(Func<object, object?>), propertyGetterAsFunc);
62+
}
4063
}
4164
else
4265
{
@@ -53,4 +76,13 @@ private static TValue CallPropertyGetter<TTarget, TValue>(
5376
{
5477
return Getter((TTarget)target);
5578
}
79+
80+
private static TValue CallPropertyGetterByReference<TTarget, TValue>(
81+
ByRefFunc<TTarget, TValue> Getter,
82+
object target)
83+
where TTarget : notnull
84+
{
85+
var unboxed = (TTarget)target;
86+
return Getter(ref unboxed);
87+
}
5688
}

src/Components/Components/test/SupplyParameterFromPersistentComponentStateValueProviderTests.cs

Lines changed: 181 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,154 @@ public async Task PersistenceFails_MultipleComponentsUseInvalidKeyTypes(object c
431431
Assert.Contains(sink.Writes, w => w is { LogLevel: LogLevel.Error } && w.EventId == new EventId(1000, "PersistenceCallbackError"));
432432
}
433433

434+
[Fact]
435+
public async Task PersistAsync_CanPersistValueTypes_IntProperty()
436+
{
437+
// Arrange
438+
var (logger, sink) = CreateTestLogger();
439+
var state = new Dictionary<string, byte[]>();
440+
var store = new TestStore(state);
441+
var persistenceManager = new ComponentStatePersistenceManager(
442+
logger,
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 - Check if there were any errors in the persistence
459+
var errors = sink.Writes.Where(w => w.LogLevel == LogLevel.Error).ToList();
460+
if (errors.Any())
461+
{
462+
var errorMessage = string.Join("; ", errors.Select(e => e.State?.ToString()));
463+
throw new InvalidOperationException($"Persistence failed with errors: {errorMessage}");
464+
}
465+
466+
Assert.NotEmpty(store.State);
467+
468+
// Verify the value was persisted correctly
469+
var newState = new PersistentComponentState(new Dictionary<string, byte[]>(), []);
470+
newState.InitializeExistingState(store.State);
471+
472+
var key = SupplyParameterFromPersistentComponentStateValueProvider.ComputeKey(componentState, cascadingParameterInfo.PropertyName);
473+
Assert.True(newState.TryTakeFromJson<int>(key, out var retrievedValue));
474+
Assert.Equal(42, retrievedValue);
475+
}
476+
477+
[Fact]
478+
public async Task PersistAsync_CanPersistValueTypes_NullableIntProperty()
479+
{
480+
// Arrange
481+
var state = new Dictionary<string, byte[]>();
482+
var store = new TestStore(state);
483+
var persistenceManager = new ComponentStatePersistenceManager(
484+
NullLogger<ComponentStatePersistenceManager>.Instance,
485+
new ServiceCollection().BuildServiceProvider());
486+
487+
var renderer = new TestRenderer();
488+
var component = new ValueTypeTestComponent { NullableIntValue = 123 };
489+
var componentStates = CreateComponentState(renderer, [(component, null)], null);
490+
var componentState = componentStates.First();
491+
492+
// Create the provider and subscribe the component
493+
var provider = new SupplyParameterFromPersistentComponentStateValueProvider(persistenceManager.State);
494+
var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(ValueTypeTestComponent.NullableIntValue), typeof(int?));
495+
provider.Subscribe(componentState, cascadingParameterInfo);
496+
497+
// Act
498+
await persistenceManager.PersistStateAsync(store, renderer);
499+
500+
// Assert
501+
Assert.NotEmpty(store.State);
502+
503+
// Verify the value was persisted correctly
504+
var newState = new PersistentComponentState(new Dictionary<string, byte[]>(), []);
505+
newState.InitializeExistingState(store.State);
506+
507+
var key = SupplyParameterFromPersistentComponentStateValueProvider.ComputeKey(componentState, cascadingParameterInfo.PropertyName);
508+
Assert.True(newState.TryTakeFromJson<int?>(key, out var retrievedValue));
509+
Assert.Equal(123, retrievedValue);
510+
}
511+
512+
[Fact]
513+
public async Task PersistAsync_CanPersistValueTypes_TupleProperty()
514+
{
515+
// Arrange
516+
var state = new Dictionary<string, byte[]>();
517+
var store = new TestStore(state);
518+
var persistenceManager = new ComponentStatePersistenceManager(
519+
NullLogger<ComponentStatePersistenceManager>.Instance,
520+
new ServiceCollection().BuildServiceProvider());
521+
522+
var renderer = new TestRenderer();
523+
var component = new ValueTypeTestComponent { TupleValue = ("test", 456) };
524+
var componentStates = CreateComponentState(renderer, [(component, null)], null);
525+
var componentState = componentStates.First();
526+
527+
// Create the provider and subscribe the component
528+
var provider = new SupplyParameterFromPersistentComponentStateValueProvider(persistenceManager.State);
529+
var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(ValueTypeTestComponent.TupleValue), typeof((string, int)));
530+
provider.Subscribe(componentState, cascadingParameterInfo);
531+
532+
// Act
533+
await persistenceManager.PersistStateAsync(store, renderer);
534+
535+
// Assert
536+
Assert.NotEmpty(store.State);
537+
538+
// Verify the value was persisted correctly
539+
var newState = new PersistentComponentState(new Dictionary<string, byte[]>(), []);
540+
newState.InitializeExistingState(store.State);
541+
542+
var key = SupplyParameterFromPersistentComponentStateValueProvider.ComputeKey(componentState, cascadingParameterInfo.PropertyName);
543+
Assert.True(newState.TryTakeFromJson<(string, int)>(key, out var retrievedValue));
544+
Assert.Equal(("test", 456), retrievedValue);
545+
}
546+
547+
[Fact]
548+
public async Task PersistAsync_CanPersistValueTypes_NullableTupleProperty()
549+
{
550+
// Arrange
551+
var state = new Dictionary<string, byte[]>();
552+
var store = new TestStore(state);
553+
var persistenceManager = new ComponentStatePersistenceManager(
554+
NullLogger<ComponentStatePersistenceManager>.Instance,
555+
new ServiceCollection().BuildServiceProvider());
556+
557+
var renderer = new TestRenderer();
558+
var component = new ValueTypeTestComponent { NullableTupleValue = ("test2", 789) };
559+
var componentStates = CreateComponentState(renderer, [(component, null)], null);
560+
var componentState = componentStates.First();
561+
562+
// Create the provider and subscribe the component
563+
var provider = new SupplyParameterFromPersistentComponentStateValueProvider(persistenceManager.State);
564+
var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(ValueTypeTestComponent.NullableTupleValue), typeof((string, int)?));
565+
provider.Subscribe(componentState, cascadingParameterInfo);
566+
567+
// Act
568+
await persistenceManager.PersistStateAsync(store, renderer);
569+
570+
// Assert
571+
Assert.NotEmpty(store.State);
572+
573+
// Verify the value was persisted correctly
574+
var newState = new PersistentComponentState(new Dictionary<string, byte[]>(), []);
575+
newState.InitializeExistingState(store.State);
576+
577+
var key = SupplyParameterFromPersistentComponentStateValueProvider.ComputeKey(componentState, cascadingParameterInfo.PropertyName);
578+
Assert.True(newState.TryTakeFromJson<(string, int)?>(key, out var retrievedValue));
579+
Assert.Equal(("test2", 789), retrievedValue);
580+
}
581+
434582
private static void InitializeState(PersistentComponentState state, List<(ComponentState componentState, string propertyName, string value)> items)
435583
{
436584
var dictionary = new Dictionary<string, byte[]>();
@@ -452,7 +600,7 @@ private static CascadingParameterInfo CreateCascadingParameterInfo(string proper
452600

453601
private static List<ComponentState> CreateComponentState(
454602
TestRenderer renderer,
455-
List<(TestComponent, object)> components,
603+
List<(IComponent, object)> components,
456604
ParentComponent parentComponent = null)
457605
{
458606
var i = 1;
@@ -464,7 +612,20 @@ private static List<ComponentState> CreateComponentState(
464612
var componentState = new ComponentState(renderer, i++, component, parentComponentState);
465613
if (currentRenderTree != null && key != null)
466614
{
467-
currentRenderTree.OpenComponent<TestComponent>(0);
615+
// Open component based on the actual component type
616+
if (component is TestComponent)
617+
{
618+
currentRenderTree.OpenComponent<TestComponent>(0);
619+
}
620+
else if (component is ValueTypeTestComponent)
621+
{
622+
currentRenderTree.OpenComponent<ValueTypeTestComponent>(0);
623+
}
624+
else
625+
{
626+
currentRenderTree.OpenComponent<IComponent>(0);
627+
}
628+
468629
var frames = currentRenderTree.GetFrames();
469630
frames.Array[frames.Count - 1].ComponentStateField = componentState;
470631
if (key != null)
@@ -497,6 +658,24 @@ private class TestComponent : IComponent
497658
public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException();
498659
}
499660

661+
private class ValueTypeTestComponent : IComponent
662+
{
663+
[SupplyParameterFromPersistentComponentState]
664+
public int IntValue { get; set; }
665+
666+
[SupplyParameterFromPersistentComponentState]
667+
public int? NullableIntValue { get; set; }
668+
669+
[SupplyParameterFromPersistentComponentState]
670+
public (string, int) TupleValue { get; set; }
671+
672+
[SupplyParameterFromPersistentComponentState]
673+
public (string, int)? NullableTupleValue { get; set; }
674+
675+
public void Attach(RenderHandle renderHandle) => throw new NotImplementedException();
676+
public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException();
677+
}
678+
500679
private class TestStore(Dictionary<string, byte[]> initialState) : IPersistentComponentStateStore
501680
{
502681
public IDictionary<string, byte[]> State { get; set; } = initialState;

0 commit comments

Comments
 (0)