diff --git a/src/Core/Infrastructure/DefaultValues.cs b/src/Core/Infrastructure/DefaultValues.cs index 01b66f299b..e06e3f1f78 100644 --- a/src/Core/Infrastructure/DefaultValues.cs +++ b/src/Core/Infrastructure/DefaultValues.cs @@ -105,16 +105,48 @@ public class DefaultValues return null; } - // Try exact type first - if (!_componentCache.TryGetValue(componentType, out var properties)) + var inheritanceChain = new List(); + + for (var currentType = componentType; currentType is not null && currentType != typeof(object); currentType = currentType.BaseType) + { + inheritanceChain.Add(currentType); + + if (currentType == typeof(FluentComponentBase)) + { + break; + } + } + + ConcurrentDictionary? mergedProperties = null; + + for (var i = inheritanceChain.Count - 1; i >= 0; i--) { - // Fallback: check open generic type definition - if (componentType.IsGenericType) + if (TryGetRegisteredProperties(inheritanceChain[i], out var properties)) { - _componentCache.TryGetValue(componentType.GetGenericTypeDefinition(), out properties); + mergedProperties ??= new ConcurrentDictionary(StringComparer.Ordinal); + foreach (var property in properties) + { + mergedProperties[property.Key] = property.Value; + } } } - return properties; + return mergedProperties; + } + + private bool TryGetRegisteredProperties(Type componentType, [NotNullWhen(true)] out ConcurrentDictionary? properties) + { + if (_componentCache.TryGetValue(componentType, out properties)) + { + return true; + } + + if (componentType.IsGenericType) + { + return _componentCache.TryGetValue(componentType.GetGenericTypeDefinition(), out properties); + } + + properties = null; + return false; } } diff --git a/src/Core/Infrastructure/DefaultValuesComponentBuilder.cs b/src/Core/Infrastructure/DefaultValuesComponentBuilder.cs index ebcb30f4e6..d9c9a8cc6d 100644 --- a/src/Core/Infrastructure/DefaultValuesComponentBuilder.cs +++ b/src/Core/Infrastructure/DefaultValuesComponentBuilder.cs @@ -50,6 +50,13 @@ public void Set(Expression> parameterSelector, var propertyName = propertyInfo?.Name ?? throw new ArgumentException($"The parameter selector '{parameterSelector}' does not resolve to a public property on the component '{typeof(TComponent)}'.", nameof(parameterSelector)); + if (!propertyInfo.CanWrite) + { + throw new ArgumentException($"The property '{propertyName}' on component '{typeof(TComponent)}' is read-only and cannot be used for default values.", nameof(parameterSelector)); + } + + ValidateValueCompatibility(propertyInfo, value, propertyName); + // Add the value to the dictionary, using the property name as the key. _values.AddOrUpdate(propertyName, AddFunction, UpdateFunction); @@ -68,4 +75,32 @@ public void Set(Expression> parameterSelector, return value; } } + + private static void ValidateValueCompatibility(PropertyInfo propertyInfo, object? value, string propertyName) + { + var propertyType = propertyInfo.PropertyType; + var nullableUnderlyingType = Nullable.GetUnderlyingType(propertyType); + var targetType = nullableUnderlyingType ?? propertyType; + + if (value is null) + { + if (propertyType.IsValueType && nullableUnderlyingType is null) + { + throw new ArgumentException($"Default value for '{propertyName}' on component '{typeof(TComponent)}' cannot be null because the property type '{propertyType}' is non-nullable.", nameof(value)); + } + + var nullabilityInfo = new NullabilityInfoContext().Create(propertyInfo); + if (nullabilityInfo.WriteState == NullabilityState.NotNull) + { + throw new ArgumentException($"Default value for '{propertyName}' on component '{typeof(TComponent)}' cannot be null because the property is non-nullable.", nameof(value)); + } + + return; + } + + if (!targetType.IsInstanceOfType(value)) + { + throw new ArgumentException($"Default value for '{propertyName}' on component '{typeof(TComponent)}' must be assignable to '{targetType}', but was '{value.GetType()}'.", nameof(value)); + } + } }