diff --git a/src/Components/Components/src/PersistentState/PersistentValueProviderComponentSubscription.cs b/src/Components/Components/src/PersistentState/PersistentValueProviderComponentSubscription.cs index 94dc09f73ad9..30e537a418a3 100644 --- a/src/Components/Components/src/PersistentState/PersistentValueProviderComponentSubscription.cs +++ b/src/Components/Components/src/PersistentState/PersistentValueProviderComponentSubscription.cs @@ -229,10 +229,12 @@ private static PropertyGetter PropertyGetterFactory((Type type, string propertyN { var (type, propertyName) = key; var propertyInfo = GetPropertyInfo(type, propertyName); - if (propertyInfo == null) + if (propertyInfo == null || propertyInfo.GetMethod == null || !propertyInfo.GetMethod.IsPublic) { - throw new InvalidOperationException($"Property {propertyName} not found on type {type.FullName}"); + throw new InvalidOperationException( + $"A public property '{propertyName}' on component type '{type.FullName}' with a public getter wasn't found."); } + return new PropertyGetter(type, propertyInfo); static PropertyInfo? GetPropertyInfo([DynamicallyAccessedMembers(LinkerFlags.Component)] Type type, string propertyName) diff --git a/src/Components/Components/test/PersistentValueProviderComponentSubscriptionTests.cs b/src/Components/Components/test/PersistentValueProviderComponentSubscriptionTests.cs index 7e6dff029752..273246516f7a 100644 --- a/src/Components/Components/test/PersistentValueProviderComponentSubscriptionTests.cs +++ b/src/Components/Components/test/PersistentValueProviderComponentSubscriptionTests.cs @@ -623,4 +623,89 @@ private class TestStore(IDictionary state) : IPersistentComponen public Task> GetPersistedStateAsync() => Task.FromResult(state); public Task PersistStateAsync(IReadOnlyDictionary state) => throw new NotImplementedException(); } + + private class ComponentWithPrivateProperty : IComponent + { + [PersistentState] + private string PrivateValue { get; set; } = "initial"; + + public void Attach(RenderHandle renderHandle) => throw new NotImplementedException(); + public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException(); + } + + private class ComponentWithPrivateGetter : IComponent + { + [PersistentState] + public string PropertyWithPrivateGetter { private get; set; } = "initial"; + + public void Attach(RenderHandle renderHandle) => throw new NotImplementedException(); + public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException(); + } + + [Fact] + public void Constructor_ThrowsClearException_ForPrivateProperty() + { + // Arrange + var state = new PersistentComponentState(new Dictionary(), [], []); + state.InitializeExistingState(new Dictionary(), RestoreContext.InitialValue); + var renderer = new TestRenderer(); + var component = new ComponentWithPrivateProperty(); + var componentState = CreateComponentState(renderer, component, null, null); + var cascadingParameterInfo = CreateCascadingParameterInfo("PrivateValue", typeof(string)); + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var logger = NullLogger.Instance; + + // Act & Assert + var exception = Assert.Throws(() => + new PersistentValueProviderComponentSubscription( + state, componentState, cascadingParameterInfo, serviceProvider, logger)); + + // Should throw a clear error about needing a public property with public getter + Assert.Contains("A public property", exception.Message); + Assert.Contains("with a public getter wasn't found", exception.Message); + } + + [Fact] + public void Constructor_ThrowsClearException_ForPrivateGetter() + { + // Arrange + var state = new PersistentComponentState(new Dictionary(), [], []); + state.InitializeExistingState(new Dictionary(), RestoreContext.InitialValue); + var renderer = new TestRenderer(); + var component = new ComponentWithPrivateGetter(); + var componentState = CreateComponentState(renderer, component, null, null); + var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(ComponentWithPrivateGetter.PropertyWithPrivateGetter), typeof(string)); + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var logger = NullLogger.Instance; + + // Act & Assert + var exception = Assert.Throws(() => + new PersistentValueProviderComponentSubscription( + state, componentState, cascadingParameterInfo, serviceProvider, logger)); + + // Should throw a clear error about needing a public property with public getter + Assert.Contains("A public property", exception.Message); + Assert.Contains("with a public getter wasn't found", exception.Message); + } + + [Fact] + public void Constructor_WorksCorrectly_ForPublicProperty() + { + // Arrange + var state = new PersistentComponentState(new Dictionary(), [], []); + state.InitializeExistingState(new Dictionary(), RestoreContext.InitialValue); + var renderer = new TestRenderer(); + var component = new TestComponent { State = "test-value" }; + var componentState = CreateComponentState(renderer, component, null, null); + var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string)); + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var logger = NullLogger.Instance; + + // Act & Assert - Should not throw + var subscription = new PersistentValueProviderComponentSubscription( + state, componentState, cascadingParameterInfo, serviceProvider, logger); + + Assert.NotNull(subscription); + subscription.Dispose(); + } }