Skip to content

Commit f9722e5

Browse files
authored
[Blazor] Fix PersistentState to throw clear error message for non-public properties (#63125)
Throw a better error message for non public properties or for properties without a public getter
1 parent e6a2648 commit f9722e5

File tree

2 files changed

+89
-2
lines changed

2 files changed

+89
-2
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,12 @@ private static PropertyGetter PropertyGetterFactory((Type type, string propertyN
229229
{
230230
var (type, propertyName) = key;
231231
var propertyInfo = GetPropertyInfo(type, propertyName);
232-
if (propertyInfo == null)
232+
if (propertyInfo == null || propertyInfo.GetMethod == null || !propertyInfo.GetMethod.IsPublic)
233233
{
234-
throw new InvalidOperationException($"Property {propertyName} not found on type {type.FullName}");
234+
throw new InvalidOperationException(
235+
$"A public property '{propertyName}' on component type '{type.FullName}' with a public getter wasn't found.");
235236
}
237+
236238
return new PropertyGetter(type, propertyInfo);
237239

238240
static PropertyInfo? GetPropertyInfo([DynamicallyAccessedMembers(LinkerFlags.Component)] Type type, string propertyName)

src/Components/Components/test/PersistentValueProviderComponentSubscriptionTests.cs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,4 +623,89 @@ 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 needing a public property with public getter
664+
Assert.Contains("A public property", exception.Message);
665+
Assert.Contains("with a public getter wasn't found", exception.Message);
666+
}
667+
668+
[Fact]
669+
public void Constructor_ThrowsClearException_ForPrivateGetter()
670+
{
671+
// Arrange
672+
var state = new PersistentComponentState(new Dictionary<string, byte[]>(), [], []);
673+
state.InitializeExistingState(new Dictionary<string, byte[]>(), RestoreContext.InitialValue);
674+
var renderer = new TestRenderer();
675+
var component = new ComponentWithPrivateGetter();
676+
var componentState = CreateComponentState(renderer, component, null, null);
677+
var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(ComponentWithPrivateGetter.PropertyWithPrivateGetter), typeof(string));
678+
var serviceProvider = new ServiceCollection().BuildServiceProvider();
679+
var logger = NullLogger.Instance;
680+
681+
// Act & Assert
682+
var exception = Assert.Throws<InvalidOperationException>(() =>
683+
new PersistentValueProviderComponentSubscription(
684+
state, componentState, cascadingParameterInfo, serviceProvider, logger));
685+
686+
// Should throw a clear error about needing a public property with public getter
687+
Assert.Contains("A public property", exception.Message);
688+
Assert.Contains("with a public getter wasn't found", exception.Message);
689+
}
690+
691+
[Fact]
692+
public void Constructor_WorksCorrectly_ForPublicProperty()
693+
{
694+
// Arrange
695+
var state = new PersistentComponentState(new Dictionary<string, byte[]>(), [], []);
696+
state.InitializeExistingState(new Dictionary<string, byte[]>(), RestoreContext.InitialValue);
697+
var renderer = new TestRenderer();
698+
var component = new TestComponent { State = "test-value" };
699+
var componentState = CreateComponentState(renderer, component, null, null);
700+
var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));
701+
var serviceProvider = new ServiceCollection().BuildServiceProvider();
702+
var logger = NullLogger.Instance;
703+
704+
// Act & Assert - Should not throw
705+
var subscription = new PersistentValueProviderComponentSubscription(
706+
state, componentState, cascadingParameterInfo, serviceProvider, logger);
707+
708+
Assert.NotNull(subscription);
709+
subscription.Dispose();
710+
}
626711
}

0 commit comments

Comments
 (0)