Skip to content

Fix PersistentState to throw clear error message for non-public properties #63125

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -623,4 +623,89 @@ private class TestStore(IDictionary<string, byte[]> state) : IPersistentComponen
public Task<IDictionary<string, byte[]>> GetPersistedStateAsync() => Task.FromResult(state);
public Task PersistStateAsync(IReadOnlyDictionary<string, byte[]> 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<string, byte[]>(), [], []);
state.InitializeExistingState(new Dictionary<string, byte[]>(), 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<InvalidOperationException>(() =>
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<string, byte[]>(), [], []);
state.InitializeExistingState(new Dictionary<string, byte[]>(), 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<InvalidOperationException>(() =>
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<string, byte[]>(), [], []);
state.InitializeExistingState(new Dictionary<string, byte[]>(), 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();
}
}
Loading