diff --git a/src/Components/Components/src/Reflection/PropertyGetter.cs b/src/Components/Components/src/Reflection/PropertyGetter.cs index ed5cd71225cc..03fa596cbc5c 100644 --- a/src/Components/Components/src/Reflection/PropertyGetter.cs +++ b/src/Components/Components/src/Reflection/PropertyGetter.cs @@ -33,8 +33,10 @@ public PropertyGetter(Type targetType, PropertyInfo property) var propertyGetterAsFunc = getMethod.CreateDelegate(typeof(Func<,>).MakeGenericType(targetType, property.PropertyType)); + var callPropertyGetterClosedGenericMethod = CallPropertyGetterOpenGenericMethod.MakeGenericMethod(targetType, property.PropertyType); + _GetterDelegate = (Func) callPropertyGetterClosedGenericMethod.CreateDelegate(typeof(Func), propertyGetterAsFunc); } @@ -46,11 +48,11 @@ public PropertyGetter(Type targetType, PropertyInfo property) public object? GetValue(object target) => _GetterDelegate(target); - private static TValue CallPropertyGetter( + private static object? CallPropertyGetter( Func Getter, object target) where TTarget : notnull { - return Getter((TTarget)target); + return (object?)Getter((TTarget)target); } } diff --git a/src/Components/Components/test/SupplyParameterFromPersistentComponentStateValueProviderTests.cs b/src/Components/Components/test/SupplyParameterFromPersistentComponentStateValueProviderTests.cs index 22fbbba1dda5..d7356855ce3a 100644 --- a/src/Components/Components/test/SupplyParameterFromPersistentComponentStateValueProviderTests.cs +++ b/src/Components/Components/test/SupplyParameterFromPersistentComponentStateValueProviderTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Components.Infrastructure; @@ -431,6 +432,146 @@ public async Task PersistenceFails_MultipleComponentsUseInvalidKeyTypes(object c Assert.Contains(sink.Writes, w => w is { LogLevel: LogLevel.Error } && w.EventId == new EventId(1000, "PersistenceCallbackError")); } + [Fact] + public async Task PersistAsync_CanPersistValueTypes_IntProperty() + { + // Arrange + var state = new Dictionary(); + var store = new TestStore(state); + var persistenceManager = new ComponentStatePersistenceManager( + NullLogger.Instance, + new ServiceCollection().BuildServiceProvider()); + + var renderer = new TestRenderer(); + var component = new ValueTypeTestComponent { IntValue = 42 }; + var componentStates = CreateComponentState(renderer, [(component, null)], null); + var componentState = componentStates.First(); + + // Create the provider and subscribe the component + var provider = new SupplyParameterFromPersistentComponentStateValueProvider(persistenceManager.State); + var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(ValueTypeTestComponent.IntValue), typeof(int)); + provider.Subscribe(componentState, cascadingParameterInfo); + + // Act + await persistenceManager.PersistStateAsync(store, renderer); + + // Assert + Assert.NotEmpty(store.State); + + // Verify the value was persisted correctly + var newState = new PersistentComponentState(new Dictionary(), []); + newState.InitializeExistingState(store.State); + + var key = SupplyParameterFromPersistentComponentStateValueProvider.ComputeKey(componentState, cascadingParameterInfo.PropertyName); + Assert.True(newState.TryTakeFromJson(key, out var retrievedValue)); + Assert.Equal(42, retrievedValue); + } + + [Fact] + public async Task PersistAsync_CanPersistValueTypes_NullableIntProperty() + { + // Arrange + var state = new Dictionary(); + var store = new TestStore(state); + var persistenceManager = new ComponentStatePersistenceManager( + NullLogger.Instance, + new ServiceCollection().BuildServiceProvider()); + + var renderer = new TestRenderer(); + var component = new ValueTypeTestComponent { NullableIntValue = 123 }; + var componentStates = CreateComponentState(renderer, [(component, null)], null); + var componentState = componentStates.First(); + + // Create the provider and subscribe the component + var provider = new SupplyParameterFromPersistentComponentStateValueProvider(persistenceManager.State); + var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(ValueTypeTestComponent.NullableIntValue), typeof(int?)); + provider.Subscribe(componentState, cascadingParameterInfo); + + // Act + await persistenceManager.PersistStateAsync(store, renderer); + + // Assert + Assert.NotEmpty(store.State); + + // Verify the value was persisted correctly + var newState = new PersistentComponentState(new Dictionary(), []); + newState.InitializeExistingState(store.State); + + var key = SupplyParameterFromPersistentComponentStateValueProvider.ComputeKey(componentState, cascadingParameterInfo.PropertyName); + Assert.True(newState.TryTakeFromJson(key, out var retrievedValue)); + Assert.Equal(123, retrievedValue); + } + + [Fact] + public async Task PersistAsync_CanPersistValueTypes_TupleProperty() + { + // Arrange + var state = new Dictionary(); + var store = new TestStore(state); + var persistenceManager = new ComponentStatePersistenceManager( + NullLogger.Instance, + new ServiceCollection().BuildServiceProvider()); + + var renderer = new TestRenderer(); + var component = new ValueTypeTestComponent { TupleValue = ("test", 456) }; + var componentStates = CreateComponentState(renderer, [(component, null)], null); + var componentState = componentStates.First(); + + // Create the provider and subscribe the component + var provider = new SupplyParameterFromPersistentComponentStateValueProvider(persistenceManager.State); + var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(ValueTypeTestComponent.TupleValue), typeof((string, int))); + provider.Subscribe(componentState, cascadingParameterInfo); + + // Act + await persistenceManager.PersistStateAsync(store, renderer); + + // Assert + Assert.NotEmpty(store.State); + + // Verify the value was persisted correctly + var newState = new PersistentComponentState(new Dictionary(), []); + newState.InitializeExistingState(store.State); + + var key = SupplyParameterFromPersistentComponentStateValueProvider.ComputeKey(componentState, cascadingParameterInfo.PropertyName); + Assert.True(newState.TryTakeFromJson<(string, int)>(key, out var retrievedValue)); + Assert.Equal(("test", 456), retrievedValue); + } + + [Fact] + public async Task PersistAsync_CanPersistValueTypes_NullableTupleProperty() + { + // Arrange + var state = new Dictionary(); + var store = new TestStore(state); + var persistenceManager = new ComponentStatePersistenceManager( + NullLogger.Instance, + new ServiceCollection().BuildServiceProvider()); + + var renderer = new TestRenderer(); + var component = new ValueTypeTestComponent { NullableTupleValue = ("test2", 789) }; + var componentStates = CreateComponentState(renderer, [(component, null)], null); + var componentState = componentStates.First(); + + // Create the provider and subscribe the component + var provider = new SupplyParameterFromPersistentComponentStateValueProvider(persistenceManager.State); + var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(ValueTypeTestComponent.NullableTupleValue), typeof((string, int)?)); + provider.Subscribe(componentState, cascadingParameterInfo); + + // Act + await persistenceManager.PersistStateAsync(store, renderer); + + // Assert + Assert.NotEmpty(store.State); + + // Verify the value was persisted correctly + var newState = new PersistentComponentState(new Dictionary(), []); + newState.InitializeExistingState(store.State); + + var key = SupplyParameterFromPersistentComponentStateValueProvider.ComputeKey(componentState, cascadingParameterInfo.PropertyName); + Assert.True(newState.TryTakeFromJson<(string, int)?>(key, out var retrievedValue)); + Assert.Equal(("test2", 789), retrievedValue); + } + private static void InitializeState(PersistentComponentState state, List<(ComponentState componentState, string propertyName, string value)> items) { var dictionary = new Dictionary(); @@ -452,7 +593,7 @@ private static CascadingParameterInfo CreateCascadingParameterInfo(string proper private static List CreateComponentState( TestRenderer renderer, - List<(TestComponent, object)> components, + List<(IComponent, object)> components, ParentComponent parentComponent = null) { var i = 1; @@ -464,7 +605,20 @@ private static List CreateComponentState( var componentState = new ComponentState(renderer, i++, component, parentComponentState); if (currentRenderTree != null && key != null) { - currentRenderTree.OpenComponent(0); + // Open component based on the actual component type + if (component is TestComponent) + { + currentRenderTree.OpenComponent(0); + } + else if (component is ValueTypeTestComponent) + { + currentRenderTree.OpenComponent(0); + } + else + { + currentRenderTree.OpenComponent(0); + } + var frames = currentRenderTree.GetFrames(); frames.Array[frames.Count - 1].ComponentStateField = componentState; if (key != null) @@ -497,6 +651,24 @@ private class TestComponent : IComponent public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException(); } + private class ValueTypeTestComponent : IComponent + { + [SupplyParameterFromPersistentComponentState] + public int IntValue { get; set; } + + [SupplyParameterFromPersistentComponentState] + public int? NullableIntValue { get; set; } + + [SupplyParameterFromPersistentComponentState] + public (string, int) TupleValue { get; set; } + + [SupplyParameterFromPersistentComponentState] + public (string, int)? NullableTupleValue { get; set; } + + public void Attach(RenderHandle renderHandle) => throw new NotImplementedException(); + public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException(); + } + private class TestStore(Dictionary initialState) : IPersistentComponentStateStore { public IDictionary State { get; set; } = initialState; diff --git a/src/Components/Shared/src/JsonSerializerOptionsProvider.cs b/src/Components/Shared/src/JsonSerializerOptionsProvider.cs index b8b60805feed..334c0927f4f6 100644 --- a/src/Components/Shared/src/JsonSerializerOptionsProvider.cs +++ b/src/Components/Shared/src/JsonSerializerOptionsProvider.cs @@ -11,5 +11,6 @@ internal static class JsonSerializerOptionsProvider { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true, + IncludeFields = true, }; } diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor index f08a419ef6f6..f21620148737 100644 --- a/src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor @@ -107,6 +107,6 @@ public class TestServiceProvider : IServiceProvider { public object GetService(Type serviceType) - => throw new NotImplementedException(); + => null; } }