From 6f8e41447a4aaf0d635ea0d5d0fa9d0bd00663a0 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Wed, 8 Jul 2026 08:37:02 +0200 Subject: [PATCH 1/2] Extend DefaultValues with option to set default value on a base class --- src/Core/Infrastructure/DefaultValues.cs | 43 ++++++++++++++++--- .../DefaultValuesComponentBuilder.cs | 29 +++++++++++++ 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/Core/Infrastructure/DefaultValues.cs b/src/Core/Infrastructure/DefaultValues.cs index 01b66f299b..ece85d3138 100644 --- a/src/Core/Infrastructure/DefaultValues.cs +++ b/src/Core/Infrastructure/DefaultValues.cs @@ -105,16 +105,47 @@ 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; + } + } + + var mergedProperties = new ConcurrentDictionary(StringComparer.Ordinal); + + 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); + foreach (var property in properties) + { + mergedProperties.AddOrUpdate(property.Key, property.Value, (_, _) => property.Value); + } } } - return properties; + return !mergedProperties.IsEmpty ? mergedProperties : null; + } + + 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..c9690f25c1 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,26 @@ 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)); + } + + 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)); + } + } } From 923717e0fd86ea4653d4ff712b7407b5cdc88972 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Wed, 8 Jul 2026 10:47:47 +0200 Subject: [PATCH 2/2] Process Copilot comments --- src/Core/Infrastructure/DefaultValues.cs | 7 ++++--- src/Core/Infrastructure/DefaultValuesComponentBuilder.cs | 6 ++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Core/Infrastructure/DefaultValues.cs b/src/Core/Infrastructure/DefaultValues.cs index ece85d3138..e06e3f1f78 100644 --- a/src/Core/Infrastructure/DefaultValues.cs +++ b/src/Core/Infrastructure/DefaultValues.cs @@ -117,20 +117,21 @@ public class DefaultValues } } - var mergedProperties = new ConcurrentDictionary(StringComparer.Ordinal); + ConcurrentDictionary? mergedProperties = null; for (var i = inheritanceChain.Count - 1; i >= 0; i--) { if (TryGetRegisteredProperties(inheritanceChain[i], out var properties)) { + mergedProperties ??= new ConcurrentDictionary(StringComparer.Ordinal); foreach (var property in properties) { - mergedProperties.AddOrUpdate(property.Key, property.Value, (_, _) => property.Value); + mergedProperties[property.Key] = property.Value; } } } - return !mergedProperties.IsEmpty ? mergedProperties : null; + return mergedProperties; } private bool TryGetRegisteredProperties(Type componentType, [NotNullWhen(true)] out ConcurrentDictionary? properties) diff --git a/src/Core/Infrastructure/DefaultValuesComponentBuilder.cs b/src/Core/Infrastructure/DefaultValuesComponentBuilder.cs index c9690f25c1..d9c9a8cc6d 100644 --- a/src/Core/Infrastructure/DefaultValuesComponentBuilder.cs +++ b/src/Core/Infrastructure/DefaultValuesComponentBuilder.cs @@ -89,6 +89,12 @@ private static void ValidateValueCompatibility(PropertyInfo propertyInfo, object 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; }