Skip to content

Commit 6788165

Browse files
Copilotjaviercn
andcommitted
Fix PersistentState to throw clear error for non-public properties
Co-authored-by: javiercn <[email protected]>
1 parent 24676a4 commit 6788165

File tree

2 files changed

+105
-1
lines changed

2 files changed

+105
-1
lines changed

src/Components/Components/src/PersistentState/PersistentValueProviderComponentSubscription.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,10 +233,28 @@ private static PropertyGetter PropertyGetterFactory((Type type, string propertyN
233233
{
234234
throw new InvalidOperationException($"Property {propertyName} not found on type {type.FullName}");
235235
}
236+
237+
// Check if the property is public
238+
if (propertyInfo.GetMethod == null || !propertyInfo.GetMethod.IsPublic)
239+
{
240+
throw new InvalidOperationException(
241+
$"The property '{propertyName}' on component type '{type.FullName}' cannot be used with PersistentState because it is not public. Properties with PersistentState must be public.");
242+
}
243+
236244
return new PropertyGetter(type, propertyInfo);
237245

238246
static PropertyInfo? GetPropertyInfo([DynamicallyAccessedMembers(LinkerFlags.Component)] Type type, string propertyName)
239-
=> type.GetProperty(propertyName);
247+
{
248+
// First try to find the property with public access only (for performance in the common case)
249+
var publicProperty = type.GetProperty(propertyName);
250+
if (publicProperty != null)
251+
{
252+
return publicProperty;
253+
}
254+
255+
// If not found as public, try with all flags to see if it exists as private/protected
256+
return type.GetProperty(propertyName, ComponentProperties.BindablePropertyFlags);
257+
}
240258
}
241259

242260
private static partial class Log

src/Components/Components/test/PersistentValueProviderComponentSubscriptionTests.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,4 +623,90 @@ private class TestStore(IDictionary<string, byte[]> state) : IPersistentComponen
623623
public Task<IDictionary<string, byte[]>> GetPersistedStateAsync() => Task.FromResult(state);
624624
public Task PersistStateAsync(IReadOnlyDictionary<string, byte[]> state) => throw new NotImplementedException();
625625
}
626+
627+
private class ComponentWithPrivateProperty : IComponent
628+
{
629+
[PersistentState]
630+
private string PrivateValue { get; set; } = "initial";
631+
632+
public void Attach(RenderHandle renderHandle) => throw new NotImplementedException();
633+
public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException();
634+
}
635+
636+
private class ComponentWithPrivateGetter : IComponent
637+
{
638+
[PersistentState]
639+
public string PropertyWithPrivateGetter { private get; set; } = "initial";
640+
641+
public void Attach(RenderHandle renderHandle) => throw new NotImplementedException();
642+
public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException();
643+
}
644+
645+
[Fact]
646+
public void Constructor_ThrowsClearException_ForPrivateProperty()
647+
{
648+
// Arrange
649+
var state = new PersistentComponentState(new Dictionary<string, byte[]>(), [], []);
650+
state.InitializeExistingState(new Dictionary<string, byte[]>(), RestoreContext.InitialValue);
651+
var renderer = new TestRenderer();
652+
var component = new ComponentWithPrivateProperty();
653+
var componentState = CreateComponentState(renderer, component, null, null);
654+
var cascadingParameterInfo = CreateCascadingParameterInfo("PrivateValue", typeof(string));
655+
var serviceProvider = new ServiceCollection().BuildServiceProvider();
656+
var logger = NullLogger.Instance;
657+
658+
// Act & Assert
659+
var exception = Assert.Throws<InvalidOperationException>(() =>
660+
new PersistentValueProviderComponentSubscription(
661+
state, componentState, cascadingParameterInfo, serviceProvider, logger));
662+
663+
// Should throw a clear error about non-public properties, not "Property not found"
664+
Assert.Contains("not public", exception.Message);
665+
Assert.Contains("PersistentState", exception.Message);
666+
Assert.DoesNotContain("not found", exception.Message);
667+
}
668+
669+
[Fact]
670+
public void Constructor_ThrowsClearException_ForPrivateGetter()
671+
{
672+
// Arrange
673+
var state = new PersistentComponentState(new Dictionary<string, byte[]>(), [], []);
674+
state.InitializeExistingState(new Dictionary<string, byte[]>(), RestoreContext.InitialValue);
675+
var renderer = new TestRenderer();
676+
var component = new ComponentWithPrivateGetter();
677+
var componentState = CreateComponentState(renderer, component, null, null);
678+
var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(ComponentWithPrivateGetter.PropertyWithPrivateGetter), typeof(string));
679+
var serviceProvider = new ServiceCollection().BuildServiceProvider();
680+
var logger = NullLogger.Instance;
681+
682+
// Act & Assert
683+
var exception = Assert.Throws<InvalidOperationException>(() =>
684+
new PersistentValueProviderComponentSubscription(
685+
state, componentState, cascadingParameterInfo, serviceProvider, logger));
686+
687+
// Should throw a clear error about non-public getter
688+
Assert.Contains("not public", exception.Message);
689+
Assert.Contains("PersistentState", exception.Message);
690+
}
691+
692+
[Fact]
693+
public void Constructor_WorksCorrectly_ForPublicProperty()
694+
{
695+
// Arrange
696+
var state = new PersistentComponentState(new Dictionary<string, byte[]>(), [], []);
697+
state.InitializeExistingState(new Dictionary<string, byte[]>(), RestoreContext.InitialValue);
698+
var renderer = new TestRenderer();
699+
var component = new TestComponent { State = "test-value" };
700+
var componentState = CreateComponentState(renderer, component, null, null);
701+
var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
702+
var serviceProvider = new ServiceCollection().BuildServiceProvider();
703+
var logger = NullLogger.Instance;
704+
705+
// Act & Assert - Should not throw
706+
var subscription = new PersistentValueProviderComponentSubscription(
707+
state, componentState, cascadingParameterInfo, serviceProvider, logger);
708+
709+
Assert.NotNull(subscription);
710+
subscription.Dispose();
711+
}
626712
}

0 commit comments

Comments
 (0)